package org.goat.caprabank.server;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.goat.caprabank.client.BankService;
import org.goat.caprabank.shared.CaprabankException;
import org.goat.caprabank.shared.entity.BankTransaction;
import org.goat.caprabank.shared.entity.User;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.util.tools.shared.Md5Utils;

public class BankServiceImpl extends RemoteServiceServlet implements BankService {

	private static final String SESSION_USER_KEY = "CaprabankUser";

	private static final Logger log = Logger.getLogger(BankServiceImpl.class.getName());

	private static final int USER_LIMIT = 30;

	private final DataStore dataStore;

	public BankServiceImpl() {
		dataStore = new DataStore();
	}

	//////////////// Public API

	@Override
	public User login(String username, String password) throws CaprabankException {
		validateNotEmpty(username, "username");
		validateNotEmpty(password, "password");
		User user = dataStore.findUserByUsername(username);
		if (user == null || !Arrays.equals(user.passwordHash, hash(password))) {
			log.warning("Failed login attempt: " + username + "/" + password + ": " + (user == null ? "user not found." : "incorrect password."));
			throw new CaprabankException("Login failed - incorrect username or password.");
		}
		log.info("Logged in user " + username + ".");
		fillDetails(user);
		setCurrentUser(user);
		return user;
	}

	@Override
	public User register(String firstName, String lastName, String email, String username, String password) throws CaprabankException {
		if (dataStore.countUsers() > USER_LIMIT) {
			log.warning("Failed registration attempt - user count exceeded.");
			throw new CaprabankException("CapraBank is not accepting new customers at the moment.");
		}
		validateNotEmpty(firstName, "first name");
		validateNotEmpty(lastName, "last name");
		validateNotEmpty(email, "email");
		validateNotEmpty(username, "username");
		validateNotEmpty(password, "password");
		if (dataStore.findUserByUsername(username) != null) {
			log.warning("Failed registration attempt - username already exists: '" + username + ".");
			throw new CaprabankException("The username is already in use.");
		}
		User newUser = new User();
		newUser.accountNum = generateId();
		newUser.firstName = firstName;
		newUser.lastName = lastName;
		newUser.email = email;
		newUser.username = username;
		newUser.passwordHash = hash(password);
		newUser.balance = 0;
		dataStore.saveUser(newUser);
		log.info("Registered user " + username + "(" + (firstName + " " + lastName) + ", " + email + ").");

		sendMail(newUser, "Welcome to CapraBank!", "Thank you for registering at CapraBank!<br/><br/>Your account number <b>" + newUser.accountNum + "</b> has been successfully created. Your username is <b>" + newUser.username + "</b>.<br/><br/>Please visit the <a href=\"http://www.caprabank.com\">CapraBank site</a> to keep track of your account.");
		setCurrentUser(newUser);
		return newUser;
	}

	@Override
	public User getDetails() throws CaprabankException {
		return getCurrentUser();
	}

	@Override
	public User transfer(long toAccount, long amount, String comment) throws CaprabankException {
		User currentUser = getCurrentUser();
		if (toAccount == currentUser.accountNum) {
			log.warning("Self-tranfer attempted by user " + currentUser.username + ".");
			throw new CaprabankException("Very funny... That's you.");
		}
		if (amount <= 0) {
			log.warning("Negative amount tranfer attempted by user " + currentUser.username + " (amount=" + formatAmount(amount) + ").");
			throw new CaprabankException("Very funny...");
		}
		User toUser = dataStore.findUserByAccountNum(toAccount);
		if (toUser == null) {
			log.warning("Tranfer to nonexistent account attempted by user " + currentUser.username + " (account=" + toAccount + ").");
			throw new CaprabankException("Account " + toAccount + " not found.");
		}

		if (!"admin".equals(currentUser.username)) {
			if (amount > currentUser.balance) {
				log.warning("Insufficient funds for tranfer attempted by user " + currentUser.username + " (amount=" + formatAmount(amount) + ", balance=" + formatAmount(currentUser.balance) + ").");
				throw new CaprabankException("Insufficient funds.");
			}
		}

		BankTransaction transferTransaction = new BankTransaction();
		transferTransaction.transactionId = generateId();
		transferTransaction.timestamp = new Date();
		transferTransaction.fromAccount = currentUser.accountNum;
		transferTransaction.toAccount = toAccount;
		transferTransaction.amount = amount;
		transferTransaction.comment = comment;
		dataStore.saveTransaction(transferTransaction);
		log.info("User " + currentUser.username + " transferred " + formatAmount(amount) + " CapraCoins to account " + toAccount + " (" + toUser.username + ") with comment '" + comment + "'.");

		String messageBody = "Your CapraBank account " + toUser.accountNum + " just received a transfer of <b>" + formatAmount(amount) + "</b> CapraCoins from account <b>" + currentUser.accountNum + "</b>.";
		if (comment != null && !comment.isEmpty()) {
			messageBody += "<br/><br/>The sender has added the following comment:<br/><b>\"" + comment + "\"</b>";
		}
		sendMail(toUser, "Your CapraBank account has received a transfer.", messageBody);
		fillDetails(currentUser);
		return currentUser;
	}

	@Override
	public void logout() {
		boolean loggedOut = false;
		HttpServletRequest request = getThreadLocalRequest();
		if (request != null) {
			HttpSession session = request.getSession();
			if (session != null) {
				User currentUser = (User) session.getAttribute(SESSION_USER_KEY);
				if (currentUser != null) {
					loggedOut = true;
					session.invalidate();
					log.info("Logged out user " + currentUser.username + ".");
				}
			}
		}
		if (!loggedOut) {
			log.severe("Logout request without an active user.");
		}
	}

	//////////////// Private helpers

	private User getCurrentUser() throws CaprabankException {
		User currentUser = null;
		HttpServletRequest request = getThreadLocalRequest();
		if (request != null) {
			currentUser = (User) request.getSession(true).getAttribute(SESSION_USER_KEY);
		}
		if (currentUser == null) {
			throw new CaprabankException("User not logged in.");
		}
		fillDetails(currentUser);
		return currentUser;
	}

	private void setCurrentUser(User user) {
		HttpServletRequest request = getThreadLocalRequest();
		if (request != null) {
			request.getSession(true).setAttribute(SESSION_USER_KEY, user);
		}
	}

	private void fillDetails(User user) {
		long balance = 0;
		List<BankTransaction> fromAccount = dataStore.findTransactionsFromAccount(user);
		for (BankTransaction transaction : fromAccount) {
			balance -= transaction.amount;
		}
		List<BankTransaction> toAccount = dataStore.findTransactionsToAccount(user);
		for (BankTransaction transaction : toAccount) {
			balance += transaction.amount;
		}
		user.transactions = new ArrayList<BankTransaction>(fromAccount);
		user.transactions.addAll(toAccount);
		user.balance = balance;
		Collections.sort(user.transactions, new Comparator<BankTransaction>() {

			@Override
			public int compare(BankTransaction o1, BankTransaction o2) {
				return o1.timestamp.compareTo(o2.timestamp);
			}
		});
	}

	private void sendMail(User toUser, String subject, String messageBody) {
		try {
			Session session = Session.getDefaultInstance(new Properties(), null);
			Message msg = new MimeMessage(session);
			msg.setFrom(new InternetAddress("admin@caprabank.com", "CapraBank"));
			String fullName = toUser.firstName + " " + toUser.lastName;
			msg.addRecipient(Message.RecipientType.TO, new InternetAddress(toUser.email, fullName));
			msg.setSubject(subject);
			msg.setContent("<html><body>Dear " + fullName + ",<br/><br/>" + messageBody + "<br/><br/>Best regards,<br/>CapraBank.</body></html>", "text/html");
			Transport.send(msg);
			log.info("Mail sent to user " + toUser.username + " (" + toUser.email + ").");
		} catch (Exception e) {
			log.log(Level.SEVERE, "Error while sending email to user " + toUser.username + " (" + toUser.email + "): " + e.getMessage(), e);
		}
	}

	//////////////// Utilities

	private byte[] hash(String input) {
		byte[] result = null;
		try {
			result = Md5Utils.getMd5Digest(input.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			// Yeah right...
		}
		return result;
	}

	private long generateId() {
		return (System.currentTimeMillis() - 1326900000000L) / 10L;
	}

	private String formatAmount(long amount) {
		return String.format("%.2f", ((double) amount / 100));
	}

	private void validateNotEmpty(String value, String name) throws CaprabankException {
		if (value == null || value.trim().isEmpty()) {
			throw new CaprabankException("The " + name + " cannot be empty.");
		}
	}
}